function TextLayout(textModel){
    this.init$Eventable();
    
    this.$textModel = textModel;
    this.$runTable = new RunTable();
    this.$tabSize = 4;
    this.$maxColumn = 0;
    this.$wrapEnabled = false;
    this.$wrapData = null;
    this.$rowCache = [];
    this.$rowCountCache = 0;
    this.$printMarginColumn = 120;
    this.$maxColumnRowIndex = -1;

    if (this.$textModel.getLength()) {
        this._updateRuns(0, 0, this.$textModel.getText());
        
    }
    this.on(this.$textModel, 'Changing', function (evt){
        this._updateRuns(evt.offset, evt.offset + evt.deletedText.length, evt.insertedText);
    });
    this.on(this.$textModel, 'LinesChanged', this._onLinesChanged);
    this._initLineData();
}
(function (){

    var _FLOOR = -1;
    var _ROUND = 0;
    var _CEIL = 1;

    var _RUN_ZERO = 0;
    var _RUN_CHAR = 1;
    var _RUN_CJK_CHAR = 2;
    var _RUN_TAB = 4;
    var _RUN_EOF = -1;

    // fields of lineData["TextLayout.data"][rowIndex]
    var $rowOffset = 0;
    var $endColumn = 1;
    //todo 当启用wrap模式时，maxColumn已经确定，可以只去layout显示部分的那些行. 当然上下的部分行也可以延迟layout，这样才能让垂直滚动条看起来正常。
    //todo 尝试将wrap部分独立出来作为WrappedTextLayout

    TextLayout.prototype = {
//+==================
//+ Run处理部分.
//+==================
        _updateRuns: function (startOffset, endOffset, text){
            this.$runTable.spliceString(startOffset, endOffset - startOffset, text);
        },
        _getRuns: function (startOffset, endOffset){
            return this.$runTable.$getRuns(startOffset, endOffset);
        },
//+==================
//+ 坐标换算部分.
//+==================
        _onLinesChanged: function (evt){
            var lineIndex = evt.startIndex;
            // 其实第一行是有offsetInline可用的
            // 这样在计算wrap时可以不用管offsetInline所在row - 1之前的row了.
            // 专门用来对付合并成为一行的文本的wrap.
            var endIndex = evt.endIndex;
            if (this.$wrapEnabled) {
                this._invalidateRowCache(lineIndex);
            }
            for (var i=lineIndex; i<=endIndex; i++) {
                this._updateLineData(i);
            }
            var maxLineIndex = this.getRowLineIndex(this.$maxColumnRowIndex)[0];
            if (lineIndex <= maxLineIndex && maxLineIndex <= evt.endIndexOld) {
                this.$maxColumnRowIndex = -1;
            }
            this._deferUpdateLayout();
        },
        _initLineData: function (){
            var lineCount = this.$textModel.getLineCount();
            for (var i=0; i<lineCount; i++) {
                this._updateLineData(i);
            }
            this._updateLayout();
        },
        _computeColumn: function (offset, startOffset){
            return this.$runTable.computeColumn(startOffset, offset - startOffset, 0);
        },
        _updateLineData: function (lineIndex){
            var data = [];
            if (!this.$wrapEnabled) {
                var endColumn = this._computeColumn(this.getRowEndOffset(lineIndex), this.getRowStartOffset(lineIndex));
                data[0] = [0, endColumn];
            }
            this.$textModel.setLineData(lineIndex, 'layoutData', data);
        },
        _updateLayout: function (){
            if (this.$maxColumnRowIndex === -1) {
                var rowCount = this.getRowCount();
                var maxRowIndex = -1;
                var maxColumn = -1;
                for (var i=0; i<rowCount; i++) {
                    var column = this.getRowEndColumn(i);
                    if (column > maxColumn) {
                        maxRowIndex = i;
                        maxColumn = column;
                    }
                }
                this.$maxColumn = maxColumn;
                this.$maxColumnRowIndex = maxRowIndex;
            }
//            this.$maxColumn = columns.reduceRight(function (initial, value){
//                return Math.max(initial, value);
//            }, 0);
            this.dispatchEvent({ type: 'LayoutChanged' });
        },
        _deferUpdateLayout: function (){
//            clearTimeout(this._updateLayoutTimer);
            var this_ = this;
//            this._updateLayoutTimer = setTimeout(function (){
                this_._updateLayout();
//            });
//            this.dispatchEvent({ type: 'LayoutChanged' });
        },
        _invalidateRowCache: function (lineIndex){
            var curLength = this.$rowCache.length;
            if (lineIndex < curLength) {
                var oldLength = this.$rowCache._oldLength;
                if (oldLength == null) {
                    this.$rowCache._oldLength = curLength;
                }
                this.$rowCache.length = lineIndex;
                if (lineIndex < oldLength >> 2) {
                    this.$rowCache = this.$rowCache.slice(0, lineIndex);
                }
            }
        },
        getRowCount: function (){
            var lineCount = this.$textModel.getLineCount();
            if (!this.$wrapEnabled) {
                return lineCount;
            }
            if (this.$rowCountCache < lineCount) {
                this.$rowCountCache = 0;
                for (var i=0; i<lineCount; i++) {
                    this.$rowCountCache += this.$wrapData[i].length;
                }
            }
            return this.$rowCountCache;
        },
        getRow: function (lineIndex){
            return lineIndex;
        },
        getRowLineIndex: function (row){
            if (!this.$wrapEnabled) {
                return [row, 0];
            }
        },
        getRowEndColumn: function (row){
            var r = this.getRowLineIndex(row);
            var lineIndex = r[0];
            var rowIndex = r[1];
//            if (this.$textModel.getLineData(lineIndex, 'dirty')) {
//
//            }
            var data = this.$textModel.getLineData(lineIndex, 'layoutData');
            return data[rowIndex][$endColumn];
        },
        getMaxColumn: function (){
            return this.$maxColumn;
        },
        getRowStartOffset: function (row){
            return this.$textModel.getLineStartOffset(row);
        },
        getRowEndOffset: function (row){
            return this.$textModel.getLineEndOffset(row);
        },
        getRowColumnOffset: function (row, column, mode){
            mode = mode | 0; // 默认为_ROUND.
            if (column == 0) {
                return [this.getRowStartOffset(row), 0];
            } else if (column >= this.getRowEndColumn(row)) {
                return [this.getRowEndOffset(row), this.getRowEndColumn(row)];
            }
            var rowStartOffset = this.getRowStartOffset(row);
            return this.$runTable.computeColumnOffset(rowStartOffset, column, mode);
        },
        getRowAndColumn: function (offset, row){
            if (row == null) {
                var rowCount = this.getRowCount();
                var left = 0;
                var right = this.getRowCount();
                while (left < right) {
                    var middle = (left + right) >> 1;
                    var next = middle + 1;
                    var middleStartOffset = this.getRowStartOffset(middle);
                    if (offset < middleStartOffset) {
                        right = middle;
                    } else {
                        if (next >= rowCount) {
                            left = right = next;
                        } else {
                            var nextRowStartOffset = this.getRowStartOffset(next);
                            if (offset > nextRowStartOffset) {
                                left = next;
                            } else if (offset == nextRowStartOffset) {
                                left = right = next;
                            } else {
                                left = right = middle;
                            }
                        }
                    }
                }
                row = left;
                // 因为之前的判断offset == nextRowStartOffset可能在最后一行时会出问题. 所以这里修一下.
                if (row >= this.getRowCount()) {
                    row = this.getRowCount() - 1;
                }
            }
            var rowStartOffset = this.getRowStartOffset(row);
            var column = this._computeColumn(offset, rowStartOffset);
            return [row, column];
        },
//+==================
//+ wrap处理部分.
//+==================
        isWrapEnabled: function (){
            return this.$wrapEnabled;
        },
        setWrapEnabled: function (val){
            this.$wrapEnabled = val;
            // todo:
        },
        getPrintMarginColumn: function (){
            return this.$printMarginColumn;
        },
        setPrintMarginColumn: function (val){
            this.$printMarginColumn = val;
        },
        // 这个方法一定是在_updateRuns后调用.
        _updateWrap: function (offset, length, text){
            if (!this.$wrapEnabled) {
                return;
            }
            
        },
//+==================
//+ 其他.
//+==================
        // todo 这个东西放在这里好讨厌啊. 考虑暴露run结构 交给外部去render
        renderInlineHTML: function (buff, startOffset, endOffset, fontInfo, row, column){
            var runs = this._getRuns(startOffset, endOffset);
            var stepColumn = column == null ? this.getRowAndColumn(startOffset, row)[1] : column;
            var stepOffset = startOffset;
            for (var i=0; i<runs.length; i++) {
                var run = runs[i];
                var runColumn = this.$runTable.computeRunColumn(run, stepColumn);
                if (run.type == _RUN_TAB) {
                    buff.push(Utils.stringRepeat('&#160;', runColumn));
                } else {
                    var text = this.$textModel.getText(stepOffset, stepOffset + run.length);
                    if (run.type == _RUN_CJK_CHAR) {
                        var letterSpacing = fontInfo.__letterSpacing || (
                            fontInfo.__letterSpacing = fontInfo.charWidth * 2 - fontInfo.cjkCharWidth);
                        if (text.length === 1) {
                            buff.push('<span style="' +
                                'margin-left:', (letterSpacing >> 1) + 1, 'px;',
                                'margin-right:', (letterSpacing >> 1) - 1, 'px;',
                                '">', text, '</span>');
                        } else {
                            buff.push('<span style="' +
                                'letter-spacing:', letterSpacing, 'px;',
                                'margin-left:', (letterSpacing >> 1) + 1, 'px;',
                                'margin-right:', -((letterSpacing >> 1) + 1), 'px',
                                '">', text, '</span>');
                        }
                    } else if (run.type == _RUN_CHAR) {
                        buff.push(Utils.escapeHTML(text));
                    }
                }
                stepOffset += run.length;
                stepColumn += runColumn;
            }
            return stepColumn;
        }
    };

})();

Utils.extend(TextLayout, Eventable);
